/*
 * ProxySession.cpp
 *
 *  Created on: Apr 23, 2012
 *      Author: shaohong
 */

#include <algorithm>
#include <sstream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "ProxySession.h"
#include "HttpHelper.h"
#include "AppLogger.h"
#include "Utils.h"

using namespace std;

static AppLogger * logger = AppLogger::getInstance("ProxySession");

static string UserAgentName = "nwprogProxy 1.0";
static string HopCountHdrName = "Hop-Count";

ProxySession::ProxySession(ProxyServer * proxyServer, int connFD, HttpRequest * httpRequest) {
	this->proxyServer = proxyServer;
	this->httpRequest = httpRequest;
	this->upstreamConn = connFD;
}

ProxySession::~ProxySession() {
	// TODO Auto-generated destructor stub
}

void ProxySession::doProxy() {

	logger->debug("domain in request: " + httpRequest->getHostNameInUrl());
	// checking domain matchings against configuration
	if (proxyServer->matchesDomain(httpRequest)) {

		// if domain matches, we work like a reverse proxy

		doReverseProxy();

	} else {
		// we work as a forwarding proxy
		doForwardProxy();
	}
}

int ProxySession::processHopCount()
{
    string hopCountValue = httpRequest->getHeaderValue(HopCountHdrName);
    if(!hopCountValue.empty()){
        int hopCount = atoi(hopCountValue.c_str());
        hopCount--;

        if(hopCount < 0){
            logger->error("Hop-Count=0!");
            sendErrorResponse(404, "Not Found!");
            return -1;
        }else{
            httpRequest->addHeader(HopCountHdrName, Utils::int2Str(hopCount));
            return 0;
        }


    } else {
    	// add the default Hop-Count
    	httpRequest->addHeader(HopCountHdrName, "3");
    }

    return 0;
}

void ProxySession::processHeaders()
{
    // when we do forward proxying, we don't change the request-uri or Host header.
    // but we need to remove some unecessary headers related to proxy
    httpRequest->removeHeader("Keep-Alive");
    httpRequest->removeHeader("Proxy-Connection");
    httpRequest->removeHeader("Cache-Control");
    httpRequest->removeHeader("Connection");
    httpRequest->removeHeader("User-Agent");
    httpRequest->userAgent = UserAgentName;
    httpRequest->addHeader("Connection", "close");
    httpRequest->addHeader("Iam", "shaohong");	// add will automatically remove the existing one
}

void ProxySession::doForwardProxy() {

	logger->debug("do forwarding proxy");

	// when we do forward proxying, we don't change the request-uri or Host header.
	// but we need to remove some unecessary headers related to proxy
	processHeaders();

	// process the "Hop-Count" header
	if (-1 == processHopCount()) {
		return;
	}

	Hop * nextHop = new Hop();
	nextHop->host = httpRequest->getDomain();
	nextHop->port = httpRequest->getPortInHostHeader();

	int nextHopFD = HttpHelper::connectToHop(nextHop);

	if (-1 == nextHopFD) {
		logger->error("failed to setup TCP connection");
		return;
	}


	string httpRequestStr = HttpHelper::createHttpRequestStr(*httpRequest);

	// now let's send out request to next hop server
	write(nextHopFD, httpRequestStr.c_str(), httpRequestStr.length());
	logger->debug("sending out HTTP request:\n%s", httpRequestStr.c_str());


	char buffer[1500];
	memset(buffer, 0, sizeof(buffer));
	if (httpRequest->isGET()) {
		// Read the initial part of the HTTP response
		int headerBytes = HttpHelper::readHttpHeaders(nextHopFD, buffer,
				sizeof(buffer));
		if (headerBytes == -1) {
			return;
		}
		logger->debug("received response:\n%s", buffer);

		//flush the response headers first
		Utils::flushOutputBuffer(upstreamConn, buffer, headerBytes);

	}

	// if the request is GET, the readingFD is the nextHopConn and
	// the writingFD is the upStreamConn
	int readingFD, writingFD;
	int contentLength = 0;

	if (httpRequest->isGET()){
		contentLength = HttpHelper::getContentLength(string(buffer));
		readingFD = nextHopFD;
		writingFD = this->upstreamConn;
	}

	if (httpRequest->isPUT()){
		contentLength = atoi(httpRequest->getHeaderValue("Content-Length").c_str());
		readingFD = this->upstreamConn;
		writingFD = nextHopFD;
	}

	vector<int> writingFDs;
	writingFDs.push_back(writingFD);

	// copy byte streams from readingFD to 1..N writing FDs
	Utils::copyByteStreams(readingFD, writingFDs, contentLength);

	close(nextHopFD);

}


void ProxySession::doReverseProxy() {

	logger->debug("do reverse proxy");

	//set the URL httpRequest to the abs_path form
	httpRequest->requestUri = httpRequest->getResourcePath();

	if (httpRequest->isGET()) {
		doReverseProxyGET();
	}

	if (httpRequest->isPUT()) {
		doReverseProxyPUT();
	}

}

void ProxySession::doReverseProxyGET() {
	//1. check if local cache has the file
	string localFilePath = proxyServer->serverConfig.cacheDir +"/"+httpRequest->getResourcePath();

	if (! HttpHelper::isDirectoryListing(*httpRequest)
		&& Utils::fileExists(localFilePath)){
		doLocalGET(localFilePath);
		return;
	}

	// let's some preprocessing before moving on
	processHeaders();

	// process the "Hop-Count" header
	if (-1 == processHopCount()) {
		return;
	}

	//2. check if any next hop server can serve this GET request
	vector<Hop *> nextHops = proxyServer->serverConfig.nextHopServers;
	HttpResponse * responseFromNextHop = NULL;
	int nextHopFD = -1;	// connection FD to next Hop

	vector<Hop*>::iterator itr = (nextHops).begin();
	for(;itr != nextHops.end(); itr++) {

		// 1. connect with target and send the request and see if we get successful response
		Hop * nextHop = *itr;
		nextHopFD = HttpHelper::connectToHop(nextHop);

		if (-1 == nextHopFD) {
			logger->error("failed to setup TCP connection");
			continue;
		}

		//let the Host header points to this Hop
		httpRequest->hostHdrValue = nextHop->toString();

		string httpRequestStr = HttpHelper::createHttpRequestStr(*httpRequest);

		// now let's send out request to next hop server
		write(nextHopFD, httpRequestStr.c_str(), httpRequestStr.length());
		logger->debug("sending to %s:%d:\n%s", nextHop->getHost().c_str(),
				nextHop->getPort(), httpRequestStr.c_str());

		char buffer[1500];
		memset(buffer, 0, sizeof(buffer));

		// Read the initial part of the HTTP response into buffer
		int headerBytes = HttpHelper::readHttpHeaders(nextHopFD, buffer,sizeof(buffer));
		if (headerBytes == -1) {
			continue;
		}
		logger->debug("received the following response:\n%s ", buffer);

		responseFromNextHop = HttpHelper::parseHttpResponse(string(buffer));
		if ( (responseFromNextHop != NULL) && (200 == responseFromNextHop->getStatusCode()) ) {
			// take short cut if we receive 200 OK from next Hop
			break;
		}

	} // end of for

	// if no response was received, let's send our own failure response
	if (NULL == responseFromNextHop) {
		sendErrorResponse(500, "Reverse proxy failed!");
		return;
	}

	// send the responseFromNextHop to the upstreamConn
	sendResponse(responseFromNextHop);

	// for successful response, we copy streams from nextHop to upstream
	if (200 == responseFromNextHop->getStatusCode()){
		int readingFD = nextHopFD;
		vector<int> writingFDs;
		writingFDs.push_back(this->upstreamConn);

		// copy byte streams from readingFD to 1..N writing FDs
		Utils::copyByteStreams(readingFD, writingFDs, responseFromNextHop->getContentLength());
	}

	// close our connections to next Hop;
	close(nextHopFD);

}

void ProxySession::doReverseProxyPUT() {
	//checking headers stuff
	processHeaders();

	// process the "Hop-Count" header
	if (-1 == processHopCount()) {
		return;
	}

	string localFilePath = proxyServer->serverConfig.cacheDir +"/"+httpRequest->getResourcePath();

	long contentLength = httpRequest->getContentLength();
	if (contentLength == 0) {
		logger->debug("PUT request is to delete file %s",
				localFilePath.c_str());

		if (remove(localFilePath.c_str()) != 0) {
			perror("Error deleting file");
			logger->error("failed to remove %s", localFilePath.c_str());
		}
	}

	int readingFD = this->upstreamConn;
	vector<int> nextHopFDs;

	// checking which next-hop servers we can connect to
	vector<Hop *> nextHops = proxyServer->serverConfig.nextHopServers;
	vector<Hop*>::iterator itr = (nextHops).begin();
	for(;itr != nextHops.end(); itr++) {

		Hop * nextHop = *itr;
		int nextHopFD = HttpHelper::connectToHop(nextHop);

		if (-1 == nextHopFD) {
			logger->error("failed to setup TCP connection to %s:%d", nextHop->host.c_str(), nextHop->port);
			continue;
		}

		// save the connection FD
		nextHopFDs.push_back(nextHopFD);

		//let the Host header points to this Hop
		httpRequest->hostHdrValue = nextHop->toString();

		// send the HTTP headers
		string httpRequestStr = HttpHelper::createHttpRequestStr(*httpRequest);

		// now let's send out request to next hop server
		write(nextHopFD, httpRequestStr.c_str(), httpRequestStr.length());
		logger->debug("sending to %s:%d:\n%s", nextHop->getHost().c_str(),
				nextHop->getPort(), httpRequestStr.c_str());

	}

	if (nextHopFDs.empty()){ // failed to connect to any next Hop servers :(
		sendErrorResponse(600, "origin servers not working!");
		return;
	}

	vector<int>writingFDs;
	for(unsigned int i=0; i<nextHopFDs.size(); i++){
		writingFDs.push_back(nextHopFDs[i]);
	}

	if (contentLength!=0){
		// open file in local cache
		int fd = open(localFilePath.c_str(), O_WRONLY|O_CREAT, S_IRWXU);
		if (fd == -1) {
			perror("Opening local file failed");
			logger->error("Failed to open %s", localFilePath.c_str());
		} else {
			writingFDs.push_back(fd);
		}

		//continue to push the contents to nextHopServers
		Utils::copyByteStreams(readingFD, writingFDs, contentLength);
	}

	//read responses from nextHops, and select the best one
	HttpResponse * bestResponse = NULL;
	for (vector<int>::iterator itr = nextHopFDs.begin();
			itr != nextHopFDs.end(); itr++){
		int nextHopFD = (*itr);
		HttpResponse * responseFromNextHop = HttpHelper::readHttpResponse(nextHopFD);
		logger->debug("received the following response:\n%s ", responseFromNextHop->generateHeadersStrings().c_str());

		if (bestResponse == NULL){
			bestResponse = responseFromNextHop;
		} else {
			if (responseFromNextHop->getStatusCode() < bestResponse->getStatusCode()) {
				bestResponse = responseFromNextHop;
			}
		}
	}

	//send response to upstream connection
	if (bestResponse == NULL){
		sendErrorResponse(600, "origin servers not working!");

		logger->error("remove local cache file since remote server doesn't like it!");

		// delete our local cache
		remove(localFilePath.c_str());

	} else {
		sendResponse(bestResponse);
	}

	// close all the FDs we opened
	for(vector<int>::iterator itr=writingFDs.begin(); itr != writingFDs.end(); itr++) {
		close(*itr);
	}
}

void ProxySession::doLocalGET(string localFilePath)
{
	int32_t fileSize = Utils::getFileSize(localFilePath.c_str());

	int fd = open(localFilePath.c_str(), O_RDONLY);

	HttpResponse * response = new HttpResponse();

	response->setStatus(200, "OK");

	// set the content length
	response->setContentLength(fileSize);

	string httpStr = response->generateHeadersStrings();

	// send response and content via upstream connection
	if (-1 == HttpHelper::pushHttpMsg(httpStr, this->upstreamConn, fd)) {
		logger->error("failed to send HTTP message!");
		return;
	}

	logger->debug("finished pushing local content to client");
}

void ProxySession::sendResponse(HttpResponse * response)
{
    string responseStr = response->generateHeadersStrings();
    Utils::flushOutputBuffer(upstreamConn, (char*)(responseStr.c_str()), (int)(responseStr.length()));
	logger->debug("sending resonse: \n%s", responseStr.c_str());

}

void ProxySession::sendErrorResponse(int responseCode, string reasonPhrase)
{
	HttpResponse * response = new HttpResponse();
	response->statusCode = responseCode;
	response->reasonPhrase = reasonPhrase;
	sendResponse(response);
}
